﻿using System;
using System.Collections.Generic;
using System.Text;

namespace CoreRay
{
    public class Triangle : GeometryObject
    {
        private GeometryBuffer geometryBuffer;
        private int triangleIndex;

        public Triangle(GeometryBuffer geometryBuffer, int triangleIndex)
        {
            // Check parameters
            if(geometryBuffer == null)
            {
                throw new ArgumentNullException("geometryBuffer");
            }

            // Init private members
            this.triangleIndex = triangleIndex * 3;
            this.geometryBuffer = geometryBuffer;

            this.HasNormalVector = (geometryBuffer.VertexFormat.Contains("Normal"));
            this.HasTextureCoordinates = (geometryBuffer.VertexFormat.Contains("TextureCoordinates"));

            // Init Bounding box
            this.BoundingBox = new BoundingBox();
            this.BoundingBox.Add(this.Vertex1Position);
            this.BoundingBox.Add(this.Vertex2Position);
            this.BoundingBox.Add(this.Vertex3Position);

            this.UseWorldMatrix = false;
        }

        public Vector3 Vertex1Position
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex), "Position");
            }
        }

        public Vector3 Vertex2Position
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 1), "Position");
            }
        }

        public Vector3 Vertex3Position
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 2), "Position");
            }
        }

        public Vector3 Vertex1Normal
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex), "Normal");
            }
        }

        public Vector3 Vertex2Normal
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 1), "Normal");
            }
        }

        public Vector3 Vertex3Normal
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector3>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 2), "Normal");
            }
        }

        public Vector2 Vertex1TextureCoordinates
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector2>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex), "TextureCoordinates");
            }
        }

        public Vector2 Vertex2TextureCoordinates
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector2>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 1), "TextureCoordinates");
            }
        }

        public Vector2 Vertex3TextureCoordinates
        {
            get
            {
                return geometryBuffer.VertexBuffer.GetVertexElement<Vector2>(this.geometryBuffer.IndexBuffer.GetVertexIndex(this.triangleIndex + 2), "TextureCoordinates");
            }
        }

        public bool HasNormalVector { get; set; }
        public bool HasTextureCoordinates { get; set; }

        public override Intersection Intersect(Ray ray)
        {
            float rayT;
            Vector3 edge1, edge2;
            float baryCentric1, baryCentric2;

            if (IntersectGeometry(ray, out rayT, out edge1, out edge2, out baryCentric1, out baryCentric2) == false)
            {
                return null;
            }

            else
            {
                // Get texture coordinates of each triangle vertices
                Vector2 vertex1TextureCoordinates = this.HasTextureCoordinates ? this.Vertex1TextureCoordinates : new Vector2(0.0f, 0.0f);
                Vector2 vertex2TextureCoordinates = this.HasTextureCoordinates ? this.Vertex2TextureCoordinates : new Vector2(1.0f, 0.0f);
                Vector2 vertex3TextureCoordinates = this.HasTextureCoordinates ? this.Vertex3TextureCoordinates : new Vector2(0.0f, 1.0f);

                // Compute deltas for triangle partial derivatives
                float du1 = vertex1TextureCoordinates.X - vertex3TextureCoordinates.X;
                float du2 = vertex2TextureCoordinates.X - vertex3TextureCoordinates.X;
                float dv1 = vertex1TextureCoordinates.Y - vertex3TextureCoordinates.Y;
                float dv2 = vertex2TextureCoordinates.Y - vertex3TextureCoordinates.Y;

                Vector3 dp1 = this.Vertex1Position - this.Vertex3Position;
                Vector3 dp2 = this.Vertex2Position - this.Vertex3Position;

                // Compute triangle partial derivatives
                Vector3 dpdu;
                Vector3 dpdv;

                float determinant = du1 * dv2 - dv1 * du2;

                if (determinant == 0.0f)
                {
                    // Handle zero determinant for triangle partial derivative matrix
                    MathUtility.CoordinateSystem(Vector3.CrossProduct(edge2, edge1).Normalize(), out dpdu, out dpdv);
                }

                else
                {
                    float invdet = 1.0f / determinant;
                    dpdu = (dp1 * dv2 - dp2 * dv1) * invdet;
                    dpdv = (dp1 * -du2 + dp2 * du1) * invdet;
                }

                Vector3 intersectionPosition = ray.ComputePoint(rayT);

                // Interpolate texture coordinates
                float baryCentric0 = 1 - baryCentric1 - baryCentric2;
                Vector2 intersectionTextureCoordinates = vertex1TextureCoordinates * baryCentric0 + vertex2TextureCoordinates * baryCentric1 + vertex3TextureCoordinates * baryCentric2;

                Vector3 normal;

                if (this.HasNormalVector)
                {
                    normal = this.Vertex1Normal * baryCentric0 + this.Vertex2Normal * baryCentric1 + this.Vertex3Normal * baryCentric2;
                    normal = normal.Normalize();
                }

                else
                {
                    normal = Vector3.CrossProduct(dpdv, dpdu).Normalize();
                }

                return new Intersection(new DifferentialGeometry(intersectionPosition, dpdu, dpdv, normal, intersectionTextureCoordinates), rayT);
            }
        }

        public override bool IntersectPosition(Ray ray)
        {
            float rayT;
            Vector3 edge1, edge2;
            float baryCentric1, baryCentric2;

            return IntersectGeometry(ray, out rayT, out edge1, out edge2, out baryCentric1, out baryCentric2);
        }

        private bool IntersectGeometry(Ray ray, out float rayT, out Vector3 edge1, out Vector3 edge2, out float baryCentric1, out float baryCentric2)
        {
            // Init output parameters
            edge1 = this.Vertex2Position - this.Vertex1Position;
            edge2 = this.Vertex3Position - this.Vertex1Position;
            rayT = 0;
            baryCentric1 = 0;
            baryCentric2 = 0;

            Vector3 normal = Vector3.CrossProduct(edge1, edge2);

            if ((normal * ray.Direction) > 0.0f)
            {
                return false;
            }
            
            Vector3 s1 = Vector3.CrossProduct(ray.Direction, edge2);

            float divisor = s1 * edge1;

            if (divisor == 0.0f)
            {
                return false;
            }

            float invDivisor = 1.0f / divisor;

            // Compute first barycentric coordinate
            Vector3 d = ray.Origin - this.Vertex1Position;
            baryCentric1 = d * s1 * invDivisor;

            if (baryCentric1 < 0.0f || baryCentric1 > 1.0f)
            {
                return false;
            }

            // Compute second barycentric coordinate
            Vector3 s2 = Vector3.CrossProduct(d, edge1);
            baryCentric2 = (ray.Direction * s2) * invDivisor;

            if (baryCentric2 < 0.0f || baryCentric1 + baryCentric2 > 1.0f)
            {
                return false;
            }

            // Compute _t_ to intersection point
            rayT = (edge2 * s2) * invDivisor;

            if (rayT < ray.MinimumT || rayT > ray.MaximumT)
            {
                return false;
            }
            
            return true;
        }
    }
}
